package com.ikingtech.platform.service.system.menu.controller;

import com.ikingtech.framework.sdk.base.model.DragOrderParam;
import com.ikingtech.framework.sdk.context.event.*;
import com.ikingtech.framework.sdk.context.exception.FrameworkException;
import com.ikingtech.framework.sdk.context.security.Me;
import com.ikingtech.framework.sdk.core.response.R;
import com.ikingtech.framework.sdk.core.support.LogHelper;
import com.ikingtech.framework.sdk.data.helper.DragHelper;
import com.ikingtech.framework.sdk.data.helper.DragHelperBuilder;
import com.ikingtech.framework.sdk.enums.common.DragTargetPositionEnum;
import com.ikingtech.framework.sdk.enums.domain.DomainEnum;
import com.ikingtech.framework.sdk.enums.system.menu.*;
import com.ikingtech.framework.sdk.log.embedded.annotation.OperationLog;
import com.ikingtech.framework.sdk.menu.api.MenuApi;
import com.ikingtech.framework.sdk.menu.api.MenuRoleApi;
import com.ikingtech.framework.sdk.menu.api.MenuUserApi;
import com.ikingtech.framework.sdk.menu.model.MenuDTO;
import com.ikingtech.framework.sdk.menu.model.MenuQueryParamDTO;
import com.ikingtech.framework.sdk.menu.model.RoleMenuAssignDTO;
import com.ikingtech.framework.sdk.utils.Tools;
import com.ikingtech.framework.sdk.web.annotation.ApiController;
import com.ikingtech.platform.service.system.menu.entity.MenuDO;
import com.ikingtech.platform.service.system.menu.exception.MenuExceptionInfo;
import com.ikingtech.platform.service.system.menu.service.MenuService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

/**
 * 系统管理-菜单模块
 * <p>1. 菜单数据需按租户隔离
 *
 * @author tie yan
 */
@Slf4j
@RequiredArgsConstructor
@ApiController(value = "/system/menu", name = "系统管理-菜单模块", description = "系统管理-菜单模块")
public class MenuController implements MenuApi {

    private final MenuService service;

    private final MenuRoleApi menuRoleApi;

    private final MenuUserApi menuUserApi;

    @Override
    @OperationLog(value = "新增菜单", dataId = "#_res.getData()")
    @Transactional(rollbackFor = Exception.class)
    public R<String> add(MenuDTO menu) {
        MenuDO entity = Tools.Bean.copy(menu, MenuDO.class);
        entity.setId(Tools.Id.uuid());
        entity.setFullPath(this.service.parseFullPath(menu.getParentId(), entity.getId()));
        entity.setPermissionCode(Tools.Str.isBlank(menu.getPermissionCode()) ? Tools.Id.uuid() : menu.getPermissionCode());
        entity.setSortOrder(this.service.getMaxSortOrder(menu.getParentId(), menu.getAppCode()) + 1);
        if (Tools.Str.isBlank(entity.getAppCode())) {
            entity.setAppCode(Me.appCode());
        }
        entity.setTenantCode(Me.tenantCode());
        this.service.save(entity);
        return R.ok(entity.getId());
    }

    @Override
    @OperationLog(value = "删除菜单")
    @Transactional(rollbackFor = Exception.class)
    public R<Object> delete(String id) {
        MenuDO entity = this.service.getById(id);
        if (null == entity) {
            LogHelper.info("MENU", "DELETE", "指定菜单不存在");
            return R.ok();
        }
        List<String> menuIds = this.service.listIdByFullPath(entity.getFullPath());
        this.service.removeByIds(menuIds);
        this.menuRoleApi.removeRoleMenu(menuIds);
        return R.ok();
    }

    @Override
    @OperationLog(value = "更新菜单")
    @Transactional(rollbackFor = Exception.class)
    public R<Object> update(MenuDTO menu) {
        MenuDO newEntity = Tools.Bean.copy(menu, MenuDO.class);
        if (!newEntity.getVisible().equals(menu.getVisible()) ||
                !Tools.Str.equals(newEntity.getMenuViewType(), menu.getMenuViewType().name())) {
            LogHelper.info("MENU", "UPDATE", "菜单<可见性>或<可视范围>变更{}", newEntity);
            this.service.updateChildMenuVisibleOrViewType(newEntity.getFullPath(), menu.getVisible(), menu.getMenuViewType().name());
        }
        this.service.updateById(newEntity);
        return R.ok();
    }

    @Override
    public R<MenuDTO> detail(String id) {
        MenuDO entity = this.service.getById(id);
        if (null == entity) {
            throw new FrameworkException(MenuExceptionInfo.MENU_NOT_FOUND);
        }
        return R.ok(this.modelConvert(entity));
    }

    @Override
    public R<List<MenuDTO>> page(MenuQueryParamDTO queryParam) {
        if (Tools.Str.isBlank(queryParam.getDomainCode())) {
            queryParam.setDomainCode(Me.domainCode());
        }
        if (Tools.Str.isBlank(queryParam.getTenantCode())) {
            queryParam.setTenantCode(Me.tenantCode());
        }
        if (Tools.Str.isBlank(queryParam.getAppCode())) {
            queryParam.setAppCode(Me.appCode());
        }
        return R.ok(this.service.listPage(queryParam).convert(this::modelConvert));
    }

    @Override
    public R<List<MenuDTO>> all() {
        return R.ok(this.modelConvert(this.service.listByDomainCodeAndTenantCodeAndAppCode(Me.domainCode(), Me.tenantCode(), Me.appCode())));
    }

    @Override
    public R<RoleMenuAssignDTO> listByRoleId(String roleId) {
        RoleMenuAssignDTO result = new RoleMenuAssignDTO();
        result.setRoleId(roleId);
        List<String> menuIds = this.menuRoleApi.loadMenuId(roleId);
        List<String> roleMenuPermissionCodes = new ArrayList<>();
        Map<String, Integer> roleAssignedMenuCountMap = new HashMap<>();
        if (Tools.Coll.isNotBlank(menuIds)) {
            List<MenuDO> roleAssignedMenuEntities = this.service.listByIds(menuIds);
            roleMenuPermissionCodes.addAll(Tools.Coll.convertList(roleAssignedMenuEntities, MenuDO::getPermissionCode));
            roleAssignedMenuCountMap.putAll(this.service.calculateMenuCount(roleAssignedMenuEntities));
        }

        List<MenuDO> entities = this.service.listByDomainCodeAndTenantCodeAndAppCode(Me.domainCode(), Me.tenantCode(), Me.appCode());
        Map<String, Integer> allMenuCountMap = this.service.calculateMenuCount(entities);
        result.setMenus(Tools.Coll.convertList(entities, entity -> {
            MenuDTO menu = this.modelConvert(entity);
            if (roleMenuPermissionCodes.contains(entity.getPermissionCode())) {
                if (Objects.equals(roleAssignedMenuCountMap.get(entity.getPermissionCode()), allMenuCountMap.get(entity.getPermissionCode()))) {
                    menu.setRoleAssigned(RoleMenuAssignedStatusEnum.CHECKED);
                } else {
                    menu.setRoleAssigned(RoleMenuAssignedStatusEnum.PARTIAL_CHECKED);
                }
            } else {
                menu.setRoleAssigned(RoleMenuAssignedStatusEnum.UNCHECKED);
            }
            return menu;
        }));

        return R.ok(result);
    }

    @Override
    public R<List<MenuDTO>> listByLoginUser() {
        if (Me.isAdmin()) {
            return R.ok(this.modelConvert(this.service.listByDomainCodeAndTenantCodeAndAppCode(Me.domainCode(), Me.tenantCode(), Me.appCode())));
        } else {
            List<String> menuIds = this.loadMenuIdsByLoginUser();
            if (Tools.Coll.isBlank(menuIds)) {
                return R.ok(new ArrayList<>());
            }
            return R.ok(this.modelConvert(this.service.listByIds(menuIds)));
        }
    }

    @Override
    public R<Object> drag(DragOrderParam dragParam) {
        MenuDO currentEntity = this.service.getById(dragParam.getCurrentId());
        if ((MenuTypeEnum.PAGE_TAB.name().equals(currentEntity.getMenuType()) ||
                MenuTypeEnum.PAGE_BUTTON.name().equals(currentEntity.getMenuType())) &&
                Tools.Str.equals(dragParam.getParentId(), dragParam.getTargetParentId())) {
            throw new FrameworkException(MenuExceptionInfo.DRAG_TAB_OR_BUTTON_TO_OTHER_MENU_NOT_ALLOWED);
        }
        MenuDO targetEntity = this.service.getById(dragParam.getTargetId());
        if (MenuTypeEnum.MENU.name().equals(currentEntity.getMenuType()) &&
                (MenuTypeEnum.PAGE_TAB.name().equals(targetEntity.getMenuType()) ||
                        MenuTypeEnum.PAGE_BUTTON.name().equals(targetEntity.getMenuType()) ||
                        MenuTypeEnum.GLOBAL_BUTTON.name().equals(targetEntity.getMenuType()))) {
            throw new FrameworkException(MenuExceptionInfo.DRAG_MENU_TO_TAB_OR_BUTTON_NOT_ALLOWED);
        }
        DragHelper<MenuDO> dragHelper = DragHelperBuilder.builder(
                        () -> this.service.gt(dragParam.getParentId(), dragParam.getCurrentOrder()),
                        () -> this.service.gt(dragParam.getTargetParentId(), dragParam.getTargetOrder()),
                        (starOrder, endOrder) -> this.service.between(dragParam.getParentId(), starOrder, endOrder)
                )
                .which(dragParam.getParentId(), dragParam.getTargetParentId(), DragTargetPositionEnum.INNER.equals(dragParam.getPosition()))
                .currentNode(() -> this.service.getById(dragParam.getCurrentId()))
                .targetNode(() -> this.service.getById(dragParam.getTargetId()))
                .maxSortOrder(() -> this.service.getMaxSortOrder(dragParam.getTargetId(), currentEntity.getAppCode()))
                .beforeTarget(DragTargetPositionEnum.BEFORE.equals(dragParam.getPosition()))
                .build();
        this.service.updateBatchById(dragHelper.drag());
        return R.ok();
    }

    @Override
    public R<String> getIdByPermissionCode(String permissionCode) {
        String menuId = this.service.getIdByPermissionCode(permissionCode, Me.tenantCode());
        if (Tools.Str.isBlank(menuId)) {
            return R.failed(MenuExceptionInfo.MENU_NOT_FOUND.message());
        }
        return R.ok(menuId);
    }

    @EventListener(SystemInitEvent.class)
    @Transactional(rollbackFor = Exception.class)
    public void systemInitEventListener() {
        LogHelper.info("MENU", "INITIALIZE", "启动时初始化平台菜单");
        if (this.service.existByDomain(DomainEnum.PLATFORM.name())) {
            LogHelper.info("MENU", "INITIALIZE", "平台菜单已初始化--跳过");
            return;
        }
        List<MenuDO> entities = Tools.Coll.flatMap(Tools.Array.convertList(PlatformMenuEnum.values(), this::createMenu));
        this.retrieveMenuParentIdAndFullPath(entities);
        if (Tools.Coll.isNotBlank(entities)) {
            this.service.saveBatch(entities);
        }
    }

    @EventListener
    @Transactional(rollbackFor = Exception.class)
    public void tenantInitEventListener(TenantInitEvent event) {
        List<MenuDO> entities = Tools.Coll.flatMap(Tools.Array.convertList(PlatformMenuEnum.values(), value -> Tools.Coll.convertList(
                value.initDomains(),
                domain -> DomainEnum.TENANT.name().equals(domain.name()),
                domain -> this.createMenu(value, domain.name(), event.getCode(), Tools.Str.EMPTY))));
        this.retrieveMenuParentIdAndFullPath(entities);
        if (Tools.Coll.isNotBlank(entities)) {
            this.service.saveBatch(entities);
        }
    }

    @EventListener
    @Transactional(rollbackFor = Exception.class)
    public void tenantDeleteEventListener(TenantDeleteEvent event) {
        List<String> menuIds = this.service.listIdByTenantCode(event.getCode());
        if (Tools.Coll.isNotBlank(menuIds)) {
            this.service.removeBatchByIds(menuIds);
            this.menuRoleApi.removeRoleMenu(menuIds);
        }
    }

    @EventListener
    @Transactional(rollbackFor = Exception.class)
    public void appMenuInitEventListener(ApplicationMenuInitEvent event) {
        List<MenuDO> entities;
        if (Tools.Coll.isBlank(event.getMenuDefinitions())) {
            List<MenuDO> previewMenuEntities = this.service.listByDomainCodeAndTenantCodeAndAppCode(DomainEnum.APPLICATION.name(), "APP_PREVIEW", event.getAppCode());
            Map<String, String> previewMenuMap = Tools.Coll.convertMap(previewMenuEntities, MenuDO::getId, MenuDO::getPermissionCode);
            entities = Tools.Coll.traverse(
                    previewMenuEntities,
                    entity -> {
                        entity.setId(Tools.Id.uuid());
                        entity.setTenantCode(event.getTenantCode());
                        entity.setDomainCode(DomainEnum.TENANT.name());
                        return entity;
                    }
            );
            Map<String, String> newMenuMap = Tools.Coll.convertMap(entities, MenuDO::getPermissionCode, MenuDO::getId);
            entities = Tools.Coll.traverse(entities, entity -> {
                entity.setParentId(newMenuMap.get(previewMenuMap.get(entity.getParentId())));
                entity.setFullPath(Tools.Coll.join(
                        Tools.Coll.traverse(
                                Tools.Str.split(entity.getFullPath(), "@"),
                                previewFullPath -> newMenuMap.get(previewMenuMap.get(previewFullPath))
                        ),
                        "@"
                ));
                return entity;
            });
        } else {
            entities = Tools.Coll.convertList(
                    event.getMenuDefinitions(),
                    menuDefinition -> this.createMenu(menuDefinition, DomainEnum.APPLICATION.name(), event.getTenantCode(), event.getAppCode())
            );
            this.retrieveMenuParentIdAndFullPath(entities);
        }
        this.service.saveBatch(entities);
    }

    @EventListener
    @Transactional(rollbackFor = Exception.class)
    public void appMenuRemoveEventListener(ApplicationMenuRemoveEvent event) {
        List<String> menuIds = this.service.listIdByDomainCodeAndTenantCodeAndAppCode(DomainEnum.TENANT.name(), event.getTenantCode(), event.getAppCode());
        if (Tools.Coll.isNotBlank(menuIds)) {
            this.service.removeBatchByIds(menuIds);
            this.menuRoleApi.removeRoleMenu(menuIds);
        }
    }

    private List<MenuDTO> modelConvert(List<MenuDO> entities) {
        return Tools.Coll.convertList(entities, this::modelConvert);
    }

    private MenuDTO modelConvert(MenuDO menuEntity) {
        MenuDTO menu = Tools.Bean.copy(menuEntity, MenuDTO.class);
        if (null != menu.getMenuType()) {
            menu.setMenuTypeName(MenuTypeEnum.valueOf(menuEntity.getMenuType()).description);
        }
        if (null != menu.getMenuViewType()) {
            menu.setMenuViewTypeName(MenuViewTypeEnum.valueOf(menuEntity.getMenuViewType()).description);
        }
        if (null != menu.getJumpType()) {
            menu.setMenuTypeName(MenuJumpTypeEnum.valueOf(menuEntity.getJumpType()).description);
        }
        return menu;
    }

    private List<String> loadMenuIdsByLoginUser() {
        List<String> roleIds = this.menuUserApi.loadRoleId(Me.id(), Me.domainCode(), Me.tenantCode(), Me.appCode());
        if (Tools.Coll.isBlank(roleIds)) {
            return new ArrayList<>();
        }
        List<String> menuIds = this.menuRoleApi.loadMenuId(roleIds);
        if (Tools.Coll.isBlank(menuIds)) {
            return new ArrayList<>();
        }
        return menuIds;
    }

    private List<MenuDO> createMenu(MenuDefinition menuDefinition) {
        return Tools.Coll.convertList(
                menuDefinition.initDomains(),
                domain -> DomainEnum.PLATFORM.name().equals(domain.name()),
                domain -> this.createMenu(menuDefinition, domain.name(), Tools.Str.EMPTY, menuDefinition.appCode()));
    }

    private MenuDO createMenu(MenuDefinition menuDefinition, String domainCode, String tenantCode, String appCode) {
        MenuDO entity = new MenuDO();
        entity.setId(Tools.Id.uuid());
        entity.setDomainCode(domainCode);
        entity.setTenantCode(tenantCode);
        entity.setAppCode(appCode);
        entity.setParentId(menuDefinition.parent());
        entity.setName(menuDefinition.description());
        entity.setEuName(menuDefinition.euName());
        entity.setPermissionCode(menuDefinition.permissionCode());
        entity.setMenuType(menuDefinition.type().name());
        entity.setJumpType(menuDefinition.jumpType().name());
        entity.setMenuViewType(MenuViewTypeEnum.MANAGE.name());
        entity.setComponent(menuDefinition.component());
        entity.setFramework(menuDefinition.framework());
        entity.setIcon(menuDefinition.icon());
        entity.setActiveIcon(menuDefinition.activeIcon());
        entity.setLogo(menuDefinition.logo());
        entity.setIframe(menuDefinition.iframe());
        entity.setDefaultOpened(false);
        entity.setPermanent(false);
        entity.setSidebar(MenuTypeEnum.MENU.equals(menuDefinition.type()));
        entity.setBreadcrumb(MenuTypeEnum.MENU.equals(menuDefinition.type()));
        entity.setActiveMenu(Tools.Str.EMPTY);
        entity.setCache(Tools.Str.EMPTY);
        entity.setNoCache(Tools.Str.EMPTY);
        entity.setBadge(Tools.Str.EMPTY);
        entity.setCopyright(false);
        entity.setPaddingBottom(Tools.Str.EMPTY);
        entity.setWhitelist(Tools.Str.EMPTY);
        entity.setLink(menuDefinition.link());
        entity.setIcon(menuDefinition.icon());
        entity.setFullPath(menuDefinition.fullPath());
        entity.setVisible(true);
        entity.setRemark(menuDefinition.description());
        entity.setSortOrder(menuDefinition.sortOrder());
        entity.setJumpType(menuDefinition.jumpType().name());
        entity.setKeepAlive(menuDefinition.keepAlive());
        return entity;
    }

    private void retrieveMenuParentIdAndFullPath(List<MenuDO> entities) {
        Map<String, List<MenuDO>> menuMap = Tools.Coll.convertGroup(entities, MenuDO::getDomainCode);
        menuMap.forEach((domainCode, domainMenuEntities) -> {
            Map<String, String> permissionCodeMap = Tools.Coll.convertMap(domainMenuEntities, MenuDO::getPermissionCode, MenuDO::getId);
            for (MenuDO entity : domainMenuEntities) {
                entity.setParentId(permissionCodeMap.get(entity.getParentId()));
                entity.setFullPath(Tools.Coll.join(Tools.Coll.traverse(Tools.Str.split(entity.getFullPath(), "@"), permissionCodeMap::get), "@"));
            }
        });
    }
}
